iT邦幫忙

2023 iThome 鐵人賽

DAY 29
0
Modern Web

React 走出新手村 系列 第 29 篇

React 走出新手村 — Rick and Morty練習(I)

  • 分享至 

  • xImage
  •  

實作練習

經過前面的介紹之後,相信大家應該有基礎的理解和認知了,接下來我們一樣透過 Rick and Morty API 來練習如何在 app router 下使用 react server component。

前面起專案的動作我就略過了,有需要的話可以參考前面的文章。

加個Navbar

在清乾淨一些 default 之後,我決定這次加個 Navbar 好了,跳轉會比較方便,那麼我一樣習慣放在 components 的資料夾下面,大家也可以照自己喜好做決定,做完大概會像下面這樣:

// src/components/Navbar.tsx
import Link from 'next/link'
import React from 'react'

const Navbar = () => {
  return (
    <nav className="flex justify-between items-center p-2 min-h-60 bg-blue-400">
      <div><Link href={`/`}>home</Link></div>
      <ul className="flex items-center">
        <li className="mx-4 bg-white rounded-lg p-2 shadow-sm"><Link href={`/charas`}>角色</Link></li>
        <li className="mx-4 bg-white rounded-lg p-2 shadow-sm"><Link href={`/locations`}>地點</Link></li>
      </ul>
    </nav>
  )
}

export default Navbar

還記得上一篇講到的 layout 嗎?我們可以把 Navbar 塞到那裡讓巢狀結構中,子層每頁都吃到它,如下:

// src/app/layout.tsx
import Navbar from '@/components/Navbar'
import './globals.css'
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'

const inter = Inter({ subsets: ['latin'] })

export const metadata: Metadata = {
  title: 'Create Next App',
  description: 'Generated by create next app',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <Navbar/>
        {children}
      </body>
    </html>
  )
}

那我們一樣先從角色清單功能開始做起,先在 app 資料夾下面新增 charas 的路徑,然後記得這裡是 app router的地盤,要照他的規定使用 page.tsx 來定義頁面喔!不然 index 是吃不到設定的。

// src/app/charas/page.tsx
import React from 'react'

const Charas = async () => 

  return (
    <div>
      charas page
    </div>
  )
}

export default Charas

現在可以不用再透過 getServerSideProps 來處理 fetch Data,可以直接對 component 下 async 然後 await 你的 fetcher function,如下:

import React from 'react'
import { RickandmortyCharacterRes } from '../../../typings'
import Link from 'next/link'
import Image from 'next/image'
const charasFetcher = async (url: string) => {
  const res = await fetch(url)
  const charasData: RickandmortyCharacterRes = await res.json()
  return charasData;
}

let currPage = `https://rickandmortyapi.com/api/character`

const Charas = async () => {
  const { info, results } = await charasFetcher(currPage) 
   
  return (
    <div>
      charas page
      {results?.map((char) => (
        <div className="flex items-center w-full my-2 p-4 shadow-xl rounded-lg" key={char.id}>
          <div className="">
            <Image src={char.image} alt={char.name} width={100} height={100}/>
          </div>
          <div>
            <p>{char.name}</p>
            <Link href={`/characters/${char.id}`}>{char.id}</Link>
          </div>
        </div>
      ))}
    </div>
  )
}

export default Charas

拿到表單以後,你會想接著應該是要處理換頁功能,說好用 useState 要在第一行加上 "use client" 的字串來取用 hooks,但如果你真的直接這麼做的話,Typescript 會提醒你不可以做這種事喔!詳細問題連結

Client components cannot be async functions.

還記得這張表嗎?

What do you need to do? Server Component Client Component
Fetch data ✅ ❌
Access backend resources (directly) ✅ ❌
Keep sensitive information on the server (access tokens, API keys, etc) ✅ ❌
Keep large dependencies on the server / Reduce client-side JavaScript ✅ ❌
Add interactivity and event listeners (onClick(), onChange(), etc) ❌ ✅
Use State and Lifecycle Effects (useState(), useReducer(), useEffect(), etc) ❌ ✅
Use browser-only APIs ❌ ✅
Use custom hooks that depend on state, effects, or browser-only APIs ❌ ✅
Use React Class components ❌ ✅
以上表格內容取自Next官方文件

所以我們應該要把功能做更細部的規劃,並往下傳遞:

// "use client" // 你不能同時用async component和hooks
import React from 'react'
import { RickandmortyCharacterRes } from '../../../typings'
import Link from 'next/link'
import Image from 'next/image'

// 現在可以不用再透過getServerSideProps來處理 fetch Data
// 可以直接對 component 下 async 然後 await 你的 fetcher
const charasFetcher = async (url: string) => {
  const res = await fetch(url)
  const charasData: RickandmortyCharacterRes = await res.json()
  return charasData;
}

let currPage = `https://rickandmortyapi.com/api/character`

// 你要思考拆出client component
const Charas = async () => {
  // 讓這裡只解決first load
  const { info, results } = await charasFetcher(currPage) 
   
  return (
    <div>
      charas page
      {results?.map((char) => (
        <div className="flex items-center w-full my-2 p-4 shadow-xl rounded-lg" key={char.id}>
          <div className="">
            <Image src={char.image} alt={char.name} width={100} height={100}/>
          </div>
          <div>
            <p>{char.name}</p>
            <Link href={`/characters/${char.id}`}>{char.id}</Link>
          </div>
        </div>
      ))}
    </div>
  )
}

export default Charas

讓我們再另外寫一個 client component 來處理需要用到 hook 的部分:

"use client"
import React, { useCallback, useState } from 'react'
import { RickandmortyCharacterRes } from '../../../typings'
import Image from 'next/image'
import Link from 'next/link'

const CharasList = ({
  listInfo
}:{
  listInfo: RickandmortyCharacterRes
}) => {
  const [pageInfo, setPageInfo] = useState({
    pageUrl:'https://rickandmortyapi.com/api/character',
    next: listInfo.info.next,
    prev: listInfo.info.prev,
    loading: false,
    curr: 1
  });
  const [charasList, setCharasList] = useState(listInfo.results);
  const pageChange = useCallback((status: string) => {
    // console.log(pageInfo);
    setPageInfo(pre => ({...pre, loading: true}));
    if (status === 'next') {
      // console.log(pageInfo);
      fetch(pageInfo.next!)
      .then((response) => response.json())
      .then((response: RickandmortyCharacterRes) => {
        const info = response.info
        setCharasList(response.results)
        setPageInfo(pre => ({
          ...pre, 
          next: info.next, 
          pageUrl: pageInfo.next!,
          prev: info.prev, 
          curr: pre.curr + 1, 
          loading: false
        }))
      });
    }
    if (status === 'prev') {
      fetch(pageInfo.prev!)
      .then((response) => response.json())
      .then((response: RickandmortyCharacterRes) => {
        const info = response.info
        setCharasList(response.results)
        setPageInfo(pre => ({
          ...pre, 
          next: info.next, 
          pageUrl: pageInfo.prev!,
          prev: info.prev, 
          curr: pre.curr - 1, 
          loading: false
        }))
      });
    }
  }, [pageInfo])
  return (
    <>
      <div className="flex justify-center items-center">
        <button 
          className="p-2 mx-2 rounded-lg shadow-md bg-blue-500 hover:bg-blue-300 disabled:bg-gray-100 min-w-40"
          onClick={() => pageChange('prev')}
          disabled={pageInfo.prev===null || pageInfo.loading} >prev</button>
        <div>{pageInfo.curr}</div>
        <button 
          className="p-2 mx-2 rounded-lg shadow-md bg-blue-500 hover:bg-blue-300 min-w-40"
          onClick={() => pageChange('next')}
          disabled={pageInfo.next===null || pageInfo.loading}>next</button>
      </div>
      {charasList?.map((char) => (
        <div className="flex items-center w-full my-2 p-4 shadow-xl rounded-lg" key={char.id}>
          <div className="">
            <Image src={char.image} alt={char.name} width={100} height={100}/>
          </div>
          <div>
            <p>{char.name}</p>
            <Link href={`/charas/${char.id}`}>{char.id}</Link>
          </div>
        </div>
      ))}
    </>
  )
}

export default CharasList

然後把原本傳入的資料修改一下:

import React from 'react'
import { RickandmortyCharacterRes } from '../../../typings'
import Link from 'next/link'
import Image from 'next/image'
import CharasList from './CharasList'

const charasFetcher = async (url: string) => {
  const res = await fetch(url)
  const charasData: RickandmortyCharacterRes = await res.json()
  return charasData;
}

let currPage = `https://rickandmortyapi.com/api/character`

// 你要思考拆出client component
const Charas = async () => {
  // 這裡只解決first load
  const charasRes = await charasFetcher(currPage) 
   
  return (
    <div>
      charas page
      <CharasList listInfo={charasRes} />
    </div>
  )
}

export default Charas

總結

那們今天的練習就差不多到這裡,明天我們來試試 Dynamic Routes 的部分。

給全新手的大禮包

React基本Hook教學


上一篇
React 走出新手村 — Next App Router
下一篇
React 走出新手村 — Rick and Morty練習(II)
系列文
React 走出新手村 共 31 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
danny101201
iT邦新手 2 級 ‧ 2023-09-29 22:55:24

恭喜要完賽了~

LucianoLee iT邦研究生 2 級 ‧ 2023-09-30 00:07:13 檢舉

感謝丹尼大的關注啊!有機會拜讀完你的文章再試試 T3 + tRpc 的實作!

哈哈我也跟你學很多 react~

我要留言

立即登入留言